package se392.ateam2006.resourcemgmt.participant.exclusionset;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Arrays;
import java.util.Collection;
import se392.ateam2006.meetingmgmt.MeetingMgmtBean;
import se392.ateam2006.resourcemgmt.ResourceMgmtBean;
import se392.ateam2006.resourcemgmt.room.RoomEntity;

import java.util.Iterator;
import se392.ateam2006.meetingmgmt.meeting.MeetingEntity;
import se392.ateam2006.meetingmgmt.meeting.StartTimeShell;
import se392.ateam2006.resourcemgmt.ResourceMgmtLocal;
import se392.ateam2006.resourcemgmt.equipment.EquipmentEntity;
import se392.ateam2006.resourcemgmt.participant.ParticipationEntity;
import se392.ateam2006.resourcemgmt.room.RoomEntity;
import se392.ateam2006.user.UserEntity;

/**
 * This class is used to resolve a time for a meeting which suits the needs of all the participants
 * The operations are designed to fulfill the requirements of Story 21
 * @author Ateam (John Adderley, Claire Melton, Matthew Bennett, Shingai Manyiwa)
 * @version 15/03/07
 */
public class MeetingTimeResolutionUtility {
    
    
    // Collection to hold all of the ExclusionRangeEntity objects we're going to deal with
    private ArrayList<ExclusionRangeEntity> exSets;
    private ResourceMgmtLocal resourceMgmt;
    private MeetingMgmtBean mbean;

    private int debug = 1;
    public static final int MEETING_UNIT_SIZE = 10;
    // Constant to define the size of a set (which is the number of 10 minute intervals in a day)
    public static final int SET_SIZE = (24 * 60) / MEETING_UNIT_SIZE;
    //The weight we give to a passive particpant
    public static final int PASSIVE_PREFERENCE_SCORE = 3;
    public static final int PASSIVE_AVAILABLE_SCORE = 1;
    public static final int PASSIVE_EXCLUSION_SCORE = 0;
    
    //GET THIS DATA FROM Management Bean.
    private final int WORKING_DAY_START = 54; //09.00 in 10 minute blocks
    private final int WORKING_DAY_END = 108; //18.00 in 10 minute blocks

    /**
     * Default constructor if we want to create an empty MeetingTimeResolutionUtility object
     */
    public MeetingTimeResolutionUtility() {

        // Initialise the exSets collection as an empty ArrayList
        exSets = new ArrayList<ExclusionRangeEntity>();
    }

    /**
     * Create a new MeetingTimeResolutionUtility object
     * @param sets - a collection of ExclusionRangeEntity objects
     */
    public MeetingTimeResolutionUtility(ArrayList<ExclusionRangeEntity> sets, ResourceMgmtLocal resourceMgmt) {
        this.resourceMgmt = resourceMgmt;
        exSets = sets;
    }

    /**
     * Add a new ExclusionRangeEntity object to the collection
     * @param set - the new ExclusionRangeEntity
     */
    public void addRange(ExclusionRangeEntity set) {

        exSets.add(set);
    }

    /**
     * Remove an ExclusionRangeEntity from the collection
     * @param set - the ExclusionRangeEntity to remove
     * @return boolean - was the range removed?
     */
    public boolean removeRange(ExclusionRangeEntity set) {

        return exSets.remove(set);
    }

    /**
     * Given each participant's exclusion/preference sets, attempt to resolve a time that suits everyone
     * @param duration - the duration (in minutes) that the meeting is required for
     * @return Calendar - the resolved meeting start time
     * Alternatively, if we can't resolve a satisfactory time, return null and deal with it elsewhere
     */
    public Collection<StartTimeShell> resolveTimes(int duration) {
        MeetingEntity meeting = exSets.get(0).getMeeting();
        // First off, let's check that each of the individual sets contains enough free time
        // Otherwise, we don't need to do anything clever as there's no point
        if (1 == debug) {
            System.err.println("ROBROBROB - Starting resolve times");
        }
        if (!(ensureSufficientTimeInAllRanges(duration))) {
            return null;
        }

        // Ok, if we're here then it may be possible to resolve a time. Let's do some more work
        int[] overlapsActive = generateRangeOverlaps(true, meeting);
        int[] overlapsPassive = generateRangeOverlaps(false, meeting);

        if (1 == debug) {
            System.err.println("ROBROBROB - Overlaps Active:");
            this.debug_printIntArray(overlapsActive);
            System.err.println("ROBROBROB - Overlaps Passive:");
            this.debug_printIntArray(overlapsPassive);
        }

        // Find how many 10 minute blocks the duration covers (want to round this up)
        int durationMask = (int) Math.ceil(duration / 10);
        // Variables to store the resolved start and end times (if we can find them...)
        //This doesn't work
        int[] resStartTimes = findResolvedStartTime(overlapsActive, durationMask);
        if (1 == debug) {
            System.err.println("ROBROBROB - findResolvedStartTime Returned");
            this.debug_printIntArray(resStartTimes);
        }
        if (resStartTimes.length<=0) {
            System.err.println("Didn't find any start times");
            return null;
        }else{
            System.out.println("ROBROBROB : System returned " + resStartTimes.length + " Start Times");
        }

        System.err.println("Found some start times");
        int orderedStartTimes[] = resolveByPopularity(overlapsPassive, resStartTimes, durationMask);
        // Now check to see if we managed to find a good start and end time
        System.err.println("first score is " + orderedStartTimes[0]);
        System.err.println("last score is " + orderedStartTimes[orderedStartTimes.length - 1]);
        if (orderedStartTimes[0] != 0) {
            System.err.println("found start times with score");
            exSets.trimToSize();
            //Get hold of all of the rooms with suitable capacity for the meeting.
            RoomEntity optimumRoom;
            ArrayList<RoomEntity> suitableRooms = findSuitableRooms();

            //Check through each of the suitable rooms until we find one
            //that has the required equipment
            ArrayList<RoomEntity> sufficientRooms = findRoomsWithEquip(suitableRooms, meeting);

            ArrayList<StartTimeShell> shells = new ArrayList<StartTimeShell>(sufficientRooms.size());
            for (int i = 0; i < shells.size(); i++) {
                int index = 0;
                int resEnd = resStartTimes[index] + durationMask;
                // Get the start date of the first exclusion set to give us a good starting point for our output date
                Calendar tmpCal1 = exSets.get(0).getStartDate();
//            // Calculate the start date and time from the resStart variable
                int start = resStartTimes[index] * 10;
                int startHour = (int) Math.floor(start / 60);
                int startMin = (start % 60);
                tmpCal1.set(Calendar.HOUR_OF_DAY, startHour);
                tmpCal1.set(Calendar.MINUTE, startMin);
                shells.add(new StartTimeShell(meeting.getId(), sufficientRooms.get(i), tmpCal1));
            }

            // Ok, we're done. Return the array of results
            return shells;
        }

        // Despite our amazing, and frankly heroic efforts, we were unable to resolve a time for the meeting... tsk.
        return null;
    }

    public ArrayList<RoomEntity> findRoomsWithEquip(ArrayList<RoomEntity> suitableRooms, MeetingEntity meeting) {
        RoomEntity room = null;
        ArrayList<RoomEntity> sufficientRooms = new ArrayList<RoomEntity>();
        for (int roomsCount = 0; roomsCount <= suitableRooms.size(); roomsCount++) {
            if (room == null) {
                RoomEntity currentRoom = suitableRooms.get(roomsCount);
                Collection<EquipmentEntity> equipmentList = resourceMgmt.getEquipmentByRoomId(currentRoom);
                ArrayList<EquipmentEntity> eql = new ArrayList<EquipmentEntity>();
                eql.addAll(equipmentList);

                if (roomSatisfiesEquipment(eql, meeting)) {
                    sufficientRooms.add(room);
                    room = currentRoom;
                }
            //We now have a list of all the equipment for this room
            //Need to go through this list and check all required
            //equipment is in this roo
            //If it is not, then we move onto the next room in the 
            //suitableRooms array, and continue until we find a room
            //with all the required equipment

            //roomFound will be set to 'true' once a suitable room is
            //found, so the loop does not continue to search for a 
            //room once we have found one


            //Equipment methods still need to be implemented - we currently
            //have no way of knowing what the users equipment requirements are
            } else {
                break;
            }
        }
        return sufficientRooms;
    }

    /**
     * Joins the overlaps passive and resStartTimes arrays to find best slots
     * sorts resStartTimes in order of best slots
     * @param overlapsPassive
     * @param resStartTimes
     * @param durationMask
     */
    public int[] resolveByPopularity(int[] overlapsPassive, int[] resStartTimes, int durationMask) {
        int[] startTimeScore = new int[resStartTimes.length];
        Arrays.fill(startTimeScore, 0);

        //This does the scoring.
        for (int i = 0; i < resStartTimes.length; i++) {
            for (int j = resStartTimes[i]; j < resStartTimes[i] + durationMask; j++) {
                startTimeScore[i] += overlapsPassive[j];
            }
        }

        //Dual bubble sort puts resStartTimes in order of highest startTimeScore
        for (int k = 1; k < startTimeScore.length; k++) {
            if (startTimeScore[k] > startTimeScore[k - 1]) {
                this.swapInts(startTimeScore, k, k - 1);
                this.swapInts(resStartTimes, k, k - 1);
                k = 0; //clever recursion happens here - go back to start condition of loop.
            }
        }

        return resStartTimes;
    }

    /**
     * Resolve a start time for a meeting (returns an index in an array (i.e. actual time is
     *      the returned value * 10
     * @param overlaps - an int[] representing the overlaps off all of the exclusion ranges
     * @param durationMask - int, the ammount of free time we need to find
     * @return int - the resolved start time for the meeting (or -1 if we couldn't resolve)
     */
    private int[] findResolvedStartTime(int[] overlapsActive, int durationMask) {
        int[] resolvedTimes = this.findResolvedStartTimeHelper(overlapsActive, durationMask, false);
        debug_printIntArray(resolvedTimes);
        if (resolvedTimes.length<=0) {
            System.out.println("Could not find a meeting time based only on preference sets.");
            resolvedTimes = this.findResolvedStartTimeHelper(overlapsActive, durationMask, true);
        }
        if (resolvedTimes.length<=0) {
            System.out.println("Could not find a meeting time based on non-exluded times");
        }
        return resolvedTimes;
    }
    
    
    /**
     * 
     * @param overlaps  -  Array of possible meeting spaces
     * @param durationMask  -  Length of meetings in MEETING_TIME_UNIT
     * @param allow_non_preference_allocation
     * @return
     */
    private int[] findResolvedStartTimeHelper(int[] overlaps, int durationMask, boolean allow_non_preference_allocation) {

        int innerLoopCounter;
        int[] resolvedTimes = new int[SET_SIZE];
        int[] returnValues;
        int resolvedTimesPointer = 0;
        int non_preference_val = (allow_non_preference_allocation) ? 0 : 1;

        Arrays.fill(resolvedTimes, -1);

        for (int i = WORKING_DAY_START; i <= ((SET_SIZE - durationMask) & (WORKING_DAY_END - durationMask)); i++) {
            if (overlaps[i] == non_preference_val || overlaps[i] == 1) {
                System.out.println("preference found at i=" + i);
                for (innerLoopCounter = i + 1; innerLoopCounter < (i + durationMask); innerLoopCounter++) {
                    if (overlaps[innerLoopCounter] != 1 && overlaps[innerLoopCounter] != non_preference_val) {
                        System.out.print(" but it's not long enough");
                        break;
                    }
                }
                if ((innerLoopCounter - i) == durationMask) {
                    System.out.println(" - found preferred space at i=" + i);
                    System.err.println("ROBROBROB - Preference Found " + "[ " + resolvedTimesPointer + ":" + i + "]");
                    resolvedTimes[resolvedTimesPointer] = i;
                    resolvedTimesPointer++;

                //It is tempting to skip i to the end of the duration period, DONT
                //This will stop it working on overlapping meeting spaces....
                }
            }
        }
        returnValues = new int[resolvedTimesPointer];
        System.err.println("ROBROBROB - returnValues array has a size of " + resolvedTimesPointer + " before trimming");
        //Sanitse so we return a valid-only array. Within the start and end times for our day.
        for (int i=WORKING_DAY_START,j=0;i<(resolvedTimes.length & WORKING_DAY_END);i++)
            if (resolvedTimes[i] != -1) returnValues[j++] = resolvedTimes[i];


        return returnValues;
    }


    public ArrayList<RoomEntity> findSuitableRooms() {
        Collection<RoomEntity> allRooms = resourceMgmt.getAllRooms();
        Iterator iter = allRooms.iterator();
        ArrayList<RoomEntity> suitableRooms = new ArrayList<RoomEntity>();
        //Iterate through all of the rooms comparing there capacity to that of the required size.
        while (iter.hasNext()) {
            RoomEntity room;
            room = (RoomEntity) iter.next();
            if (room.getCapacity() >= exSets.size()) {
                suitableRooms.add(room);
            }
        }
        //Sort the array of suitable rooms, in order of capacity, from lowest to highest to
        //optimise room usage, i.e. not booking a room that is larger than necessary if a smaller
        //room is suitable
        if (suitableRooms.size() != 0) {
            RoomEntity tempRoom;
            for (int i = 1; i < suitableRooms.size(); i++) {
                if (suitableRooms.get(i).getCapacity() < suitableRooms.get(i - 1).getCapacity()) {
                    tempRoom = suitableRooms.get(i);
                    suitableRooms.set(i, suitableRooms.get(i - 1));
                    suitableRooms.set(i - 1, tempRoom);
                }
            }
        }

        return suitableRooms;
    }

    /**
     * Create an array of ints representing all of the preference and exclusion sets overlayed
     * @return int[] - the result of the overlay
     */
    private int[] generateRangeOverlaps(Boolean isActive, MeetingEntity theMeeting) {

        int[] out = new int[SET_SIZE];

        // Fill the output array with 1's (see loop later)
        Arrays.fill(out, 1);

        // Loop round each index in the array
        for (int i = 0; i < SET_SIZE; i++) {

            // Loop round each of the exclusion/preference sets
            for (ExclusionRangeEntity ex : exSets) {
                //if anything in this column is less than the current value in our output array, make
                //the value in the output array equal to it. So, any -1's in a column will make the output
                //for that column -1, if not, then any 0's will make tha output 0, else the output will be 1

                //Although M.Fowler says "No Temp Varialbles" I think they should be used here, as we have too many calls.
                if (isActive) {
                    UserEntity theUser = ex.getUserEnt();
                    //this causes a null pointer exception
                    ParticipationEntity pet = resourceMgmt.getParticipant(theUser, theMeeting);
                    if (!pet.isActive()) {
                        continue;
                    }
                    if (out[i] > ex.toIntArray()[i]) {
                        out[i] = ex.toIntArray()[i];
                    }
                } else {
                    //Dealing with passive participants, counting how many can make a slot
                    if (ex.toIntArray()[i] == 1) {
                        out[i] += PASSIVE_PREFERENCE_SCORE;
                    }
                    if (ex.toIntArray()[i] == 0) {
                        out[i] += PASSIVE_AVAILABLE_SCORE;
                    }
                    if (ex.toIntArray()[i] == -1) {
                        out[i] += PASSIVE_EXCLUSION_SCORE;
                    }
                }
            }
        }

        System.out.println(Arrays.toString(out));
        return out;
    }

    /**
     * Ensure that there is sufficient free time available in all of the exclusion ranges
     * @param duration - the duration of the required meeting (in minutes)
     * @return boolean - true if there is enough time, false if not
     */
    private boolean ensureSufficientTimeInAllRanges(int duration) {

        // Loop round each exclusion range and check them
        for (ExclusionRangeEntity row : exSets) {

            if (!(ensureSufficientTime(row, duration))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Perform a quick test to ensure that there is enough free time in an exclusion range to allow for a meeting
     * @param range - the exclusion range
     * @param duration - the amount of time that the meeting needs to be for (in minutes)
     * @return boolean - true if there is enough time, false if not
     */
    private boolean ensureSufficientTime(ExclusionRangeEntity range, int duration) {

        // Get the start and end times in minutes
        int start = range.getStartTimeInMins();
        int end = range.getEndTimeInMins();

        // Check if there's enough time for the meeting
        return ((end - start) < duration) ? false : true;
    }

    private void swapInts(int[] a, int indexA, int indexB) {
        int tempInt = a[indexA];
        a[indexA] = a[indexB];
        a[indexB] = tempInt;
    }

    private int containsAt(String[] a, String b) {
        for (int i = 0; i < a.length; i++) {
            if (a[i].equals(b)) {
                return i;
            }
        }
        return -1;
    }

    private boolean roomSatisfiesEquipment(ArrayList<EquipmentEntity> roomEquipment, MeetingEntity m) {
        String[] meetingEquipments = m.getEquipment().split(",");
        ArrayList<EquipmentEntity> localRoomEquipment = new ArrayList<EquipmentEntity>();
        localRoomEquipment.addAll(roomEquipment);


        int toResolve = meetingEquipments.length;
        for (int i = 0; i < meetingEquipments.length; i++) {
            for (int j = 0; j < localRoomEquipment.size(); j++) {
                if (meetingEquipments[i].equalsIgnoreCase(roomEquipment.get(j).getType())) {
                    localRoomEquipment.remove(j);
                    toResolve--;
                    break;
                }
            }
            if (toResolve == 0) {
                return true;
            } else {
                toResolve = meetingEquipments.length;
            }
        }

        return false;
    }

    private void debug_printIntArray(int[] a) {
        for (int i = 0; i < a.length; System.out.print("[" + i + ":" + a[i++] + "]")) {
            continue;
        }
        System.out.println("");
    }
}

